TypeScript type 和 interface 的异同及其使用
子类型的分类
编程语言的子类型分为两种:名义子类型和结构子类型。
名义子类型就是指,例如 Java 中类的继承,子类就是父类的子类型,而要建立父子类的关系只有一个办法就是 extends
(接口的 implements
也算),只有用了 extends
才会出现父子类型,也就是只能用 extends
才能让他们成为名义上的父子类型。
结构子类型就是结构相同即可,而 TypeScript 就是结构子类型,即在 TypeScript 中
type Foo = {
a: string
}
// 和
type Bar = {
a: string
}
// 这两个都是一样类型
不需要 extends(尽管 extends 也可以创造子类型,但本质是结构相似)。所以
type Foo = {
a: string
}
type Bar = {
a: string,
b: number
}
中 Bar 是 Foo 的子类型。
相似的地方
对象自变量式的结构定义
interface Foo {
a: string
}
和
type Foo {
a: string
}
都是定义了一个有 a 属性的对象结构。
函数类型
函数类型其实由两个部分构成,参数类型和返回值类型。
interface Foo {
(a: string): string
}
和
type Foo = (a: string) => string
混合类型
由于 JavaScript 具有动态和灵活的性质,有时可能会遇到一个对象,该对象可以作为上述某些类型的组合使用,就是一个既具有函数特性又具有对象特性的类型,它具有一些属性。如下:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = (function (start: number) { }) as Counter;
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
当然其中的
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
也可以使用 type 实现 :
type Counter = {
(start: number): string;
interval: number;
reset(): void;
}
关于 | 和 & 操作符的使用
type Foo = {
a: string
}
type Bar = {
b: number
}
type Baz = Foo & Bar
和
interface Foo {
a: string
}
interface Bar {
b: number
}
type Baz = Foo & Bar
同样 |
操作符也是
&
和 |
操作符都会创建一个新的类型,而且是相关类型父子类型链上的类型。它们区别是他们的产物是在父子类型链上的不同角色。如上所示,第一段代码中,对 Foo 和 Bar 使用了 &
操作符,创建的新的类型 Baz,结果是 Baz 是 Foo 和 Bar 的子类型,就是说,Bar 类型的变量何以赋值给 Foo 和 Bar 类型的变量。在第二段代码中,对 Foo 和 Bar 使用了|操作符,创建的新的类型 Baz,结果是 Baz 是 Foo 和 Bar 的父类型,即,Foo 和 Bar 类型的变量可以赋值给 Bar。
索引类型
interface Foo {
[x: string]: number
[x: number]: string
}
和
type Foo = {
[x: string]: number
[x: number]: string
}
都表示索引为 string 的属性的类型都为 number,索引为 number 的属性的类型都为 string。
不同的地方
type 类型别称
很简单,就是为已经存在的类型创建另一个名字,代表完全相同的意义。例如:
type ObjectAlias = object
虽然说这个特性是 type
独有的,但当原类型不是原始类型时,即原类型不是 number、string、boolean、object、symbol、null、undefined、void、never、unknown、any 时,interface 可以使用以下方式实现类似的功能:
interface Foo {
a: string
}
interface FooAlias extends Foo {}
扩展接口
interface Foo {
a: string
}
interface Bar extends Foo {
b: number
}
type
使用 &
可以实现类似的效果,如下:
interface Foo {
a: string
}
type Bar = Foo & {
b: number
}
补充:接口的构造函数
构造签名
在 TypeScript 接口中,你可以使用 new
关键字来描述一个构造函数:
interface Point {
new (x: number, y: number): Point;
}
以上接口中的 new (x: number, y: number)
我们称之为构造签名
与该语法相对应的几种常见的使用形式如下:
new C
new C ( ... )
new C < ... > ( ... )
构造函数类型
构造函数类型字面量是包含单个构造函数签名的对象类型的简写。具体来说,构造函数类型字面量的形式如下:
new < T1, T2, ... > ( p1, p2, ... ) => R
该形式与以下对象字面量类型是等价的:
{ new < T1, T2, ... > ( p1, p2, ... ) : R }
下面我们来举个实际的示例:
// 构造函数字面量
new (x: number, y: number) => Point
等价于以下对象类型字面量:
{
new (x: number, y: number): Point;
}
构造函数类型的应用
在介绍构造函数类型的应用前,我们先来看个例子:
interface Point {
new (x: number, y: number): Point;
x: number;
y: number;
}
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const point: Point = new Point2D(1, 2);
对于以上的代码,TypeScript 编译器会提示以下错误信息:
Class 'Point2D' incorrectly implements interface 'Point'.
Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.
要解决这个问题,我们就需要把对前面定义的 Point 接口进行分离,即把接口的属性和构造函数类型进行分离:
interface Point {
x: number;
y: number;
}
interface PointConstructor {
new (x: number, y: number): Point;
}
完成接口拆分之后,除了前面已经定义的 Point2D 类之外,我们又定义了一个 newPoint 工厂函数,该函数用于根据传入的 PointConstructor 类型的构造函数,来创建对应的 Point 对象。
class Point2D implements Point {
readonly x: number;
readonly y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function newPoint(
pointConstructor: PointConstructor,
x: number,
y: number
): Point {
return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 2, 2);